/**
 * LocalContrastFilter.java
 * 
 * @date: 2012-02-09
 * @author: Xiaoyu Guo (MUN# 200982155)
 * This file is part of the course project for COMP 6752, 
 * Application of Computer Graphics. All the file is written
 * by myself.
 */
package mun.cs6752.project.filter.localContrast;

import java.awt.image.BufferedImage;

import mun.cs6752.project.filter.abstractClasses.AbstractImageFilter;
import mun.cs6752.project.filter.abstractClasses.AbstractImageFilterControl;
import mun.cs6752.project.filter.interfaces.IImageFilterParameter;
import mun.cs6752.project.filter.interfaces.IImageProvider;
import mun.cs6752.project.filter.localContrast.LocalContrastFilterParameter.Mode;

/**
 * @author Xiaoyu Guo
 *
 */
public class LocalContrastFilter extends AbstractImageFilter {

    // fields for caching the image stats
    int[][] cachedR, cachedG, cachedB;          // accumulated R, G, B
    int[][] cachedR2, cachedG2, cachedB2;       // accumulated square of R, G, B
    int[][] cachedItems;                        // accumulated number of pixels
    int[][] cachedRGB;                          // cached RGB from last image
    BufferedImage cachedImage;
    int cachedRadius;
    
    private enum Channel{
        Red, Green, Blue
    }
    
    /**
     * @param provider
     * @param name
     */
    public LocalContrastFilter(IImageProvider provider) {
        super(provider, "Local Contrast");
    }
    
    /* (non-Javadoc)
     * @see mun.cs6752.project.filter.interfaces.IImageFilter#execute(java.awt.image.BufferedImage, mun.cs6752.project.filter.interfaces.IImageFilterParameter)
     */
    @Override
    public BufferedImage execute(BufferedImage srcImage, IImageFilterParameter param) {
        LocalContrastFilterParameter p = null;
        
        if(param == null || !(param instanceof LocalContrastFilterParameter)){
            p = new LocalContrastFilterParameter();
        } else {
            p = (LocalContrastFilterParameter) param;
        }
        
        int radius = p.getMeanSize();
        
        final int width = srcImage.getWidth();
        final int height = srcImage.getHeight();
        
        if(!isCachedImage(srcImage, p)){
            resetCache(srcImage, p);
            fillColorStat(srcImage, radius);
        }
        
        // thresholding
        
        BufferedImage destImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for(int y = 0; y < height; y++){
            for(int x = 0; x < width; x++){
                int rgb = srcImage.getRGB(x, y);
                int r = getR(rgb);
                int g = getG(rgb);
                int b = getB(rgb);
                
                int thresholdR = (int)getThreshold(p, Channel.Red, x, y);
                int thresholdG = thresholdR;
                int thresholdB = thresholdR;
                
                if(p.getMode().equals(Mode.Color)){
                    thresholdG = (int)getThreshold(p, Channel.Green, x, y);
                    thresholdB = (int)getThreshold(p, Channel.Blue, x, y);
                }
                
                r = (r > thresholdR) ? 255 : 0;
                g = (g > thresholdG) ? 255 : 0;
                b = (b > thresholdB) ? 255 : 0;
                
                rgb = getRGB(r, g, b);
                destImage.setRGB(x, y, rgb);
            }
        }
        
        return destImage;
    }
    
    /**
     * @param param
     * @param channel
     * @param x
     * @param y
     * @return
     */
    private double getThreshold(LocalContrastFilterParameter param, Channel channel, int x, int y){
        double result = 0.0;
        int[][] accu = cachedR;
        int[][] square = cachedR2;
        if(param.getMode().equals(Mode.Color)){
            if(channel.equals(Channel.Green)){
                accu = cachedG;
                square = cachedG2;
            } else if (channel.equals(Channel.Blue)){
                accu = cachedB;
                square = cachedB2;
            }
        }
        
        switch(param.getSelectedMethod()){
        case Sauvola:
            result = getSauvola(
                getMean(accu, cachedItems, x, y), 
                getStdc(accu, square, cachedItems, x, y),
                param.getK(), 
                param.getR()
            );
            break;
        case Niblack:
            result = getNiblack(
                getMean(accu, cachedItems, x, y), 
                getStdc(accu, square, cachedItems, x, y),
                param.getK(), 
                param.getR()
            );
            break;
        }
        return result;
    }
    
    private boolean isCachedImage(BufferedImage image, LocalContrastFilterParameter param){
        if(cachedImage == null){
            return false;
        }
        
        // assume once cached, all cached values should be valid
        
        // the case of the new image is null or empty
        if(image == null || image.getWidth() <= 0 || image.getHeight() <= 0){
            return false;
        }
        // the case that the size of image is not identical
        int height = image.getHeight();
        int width = image.getWidth();
        if(height != cachedRGB.length || width != cachedRGB[0].length){
            return false;
        }
        
        // the worst case that image is the same but rgb has changed
        for(int y = 0; y < height; y++){
            for(int x = 0; x < width; x++){
                if(image.getRGB(x, y) != cachedRGB[y][x]){
                    return false;
                }
            }
        }
        
        return true;
    }
    
    private void resetCache(BufferedImage image, LocalContrastFilterParameter param){
        int width = image.getWidth();
        int height = image.getHeight();
        cachedR = new int[height][width];
        cachedG = new int[height][width];
        cachedB = new int[height][width];
        cachedR2 = new int[height][width];
        cachedG2 = new int[height][width];
        cachedB2 = new int[height][width];
        cachedItems = new int[height][width];
        cachedRGB = new int[height][width];
        cachedImage = image;
        cachedRadius = param.getMeanSize();
    }
    
    private void fillColorStat(BufferedImage srcImage, int radius){
        final int width = srcImage.getWidth();
        final int height = srcImage.getHeight();
        
        for(int y = 0; y < height; y++){
            for(int x = 0; x < width; x++){
                int left = x - radius;
                int right = x + radius;
                int top = y - radius;
                int bottom = y + radius;
            
                int rgb = srcImage.getRGB(x, y);
                int r = getR(rgb);
                int g = getG(rgb);
                int b = getB(rgb);
                int r2 = r * r;
                int g2 = g * g;
                int b2 = b * b;
                cachedRGB[y][x] = rgb;
                
                for(int py = top; py < bottom; py++){
                    if(py >= 0 && py < height){
                        for(int px = left; px < right; px++){
                            if(px >= 0 && px < width){
                                cachedR[py][px] += r;
                                cachedG[py][px] += g;
                                cachedB[py][px] += b;
                                cachedR2[py][px] += r2;
                                cachedG2[py][px] += g2;
                                cachedB2[py][px] += b2;
                                cachedItems[py][px] ++;
                            }
                        }
                    }
                }
            }
        }
    }
    
    private double getSauvola(final double mean, final double stdc, final double paramK, final double paramR){
        return mean * (1.0 + paramK * (stdc / paramR - 1.0) );
    }
    private double getNiblack(final double mean, final double stdc, final double paramK, final double paramC){
        return mean + paramK * stdc - paramC;
    }
    
    private double getStdc(final int[][] accu, final int[][] square, final int[][] count, final int x, final int y){
//        stdc = sqrt(mean(x^2) - (mean(x))^2)
        return Math.sqrt((double)square[y][x] / (double)count[y][x] - Math.pow(accu[y][x] / count[y][x], 2.0));
    }
    
    private double getMean(final int[][] accu, final int[][] count, final int x, final int y){
        return (double)accu[y][x] / (double)count[y][x];
    }
    
    /* (non-Javadoc)
     * @see mun.cs6752.project.filter.interfaces.IImageFilter#getControl()
     */
    @Override
    public AbstractImageFilterControl getControl() {
        return new LocalContrastFilterControl(provider, this);
    }

}
